package com.moralesce.gt.math;

import static com.moralesce.gt.util.ListUtil.merge;
import static org.apache.commons.collections.CollectionUtils.containsAny;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.moralesce.gt.model.Board;
import com.moralesce.gt.model.Card;
import com.moralesce.gt.model.Deck;
import com.moralesce.gt.model.Hand;
import com.moralesce.gt.model.StartingHand;
import com.moralesce.gt.util.ListUtil;

/**
 * Foundational enumerations.
 * 
 * <p>
 * To study probabilities and statistics in poker, common types of data sets can
 * be used used in Monte Carlo style simulations or direct computations.
 * </p>
 * 
 * <p>
 * This class makes the following data sets available:<br/>
 * - Every 5-card poker hand.<br/>
 * - Every 5-card poker hand that can be made with a specified starting hand.<br/>
 * - Every 5-card poker hand that can be made with a specified starting hand
 * after the flop.<br/>
 * - Every 5-card poker hand that can be made with a specified starting hand
 * after the turn.<br/>
 * - Every 5-card poker hand that can be made with a specified starting hand
 * after the river.<br/>
 * </p>
 * 
 * @author Brandon Grenier
 */
public final class Enumerations {

	private Enumerations() {

	}

	/**
	 * Returns all possible 5-card poker hands.
	 * 
	 * <p>
	 * This method returns a list of all possible 5-card poker hands. There are
	 * (52c5) 2,598,960 5-card poker hands.
	 * </p>
	 * 
	 * @return A List<Hand> of all possible 5-card poker hands.
	 */
	public static List<Hand> allHands() {
		return toHands(new Combination(new Deck().cards(), 5).list());
	}

	/**
	 * Returns all possible 5-card poker hands that could be made with the given
	 * starting hand.
	 * 
	 * <p>
	 * A deck will contain 50 cards after one starting hand has been dealt.
	 * There will be exactly 19,600 (50c3) 5-card poker hands for any given
	 * starting hand. This method returns a list containing every 5-card poker
	 * hand that can be made with this starting hand.
	 * </p>
	 * 
	 * @return A List<Hand> of all possible 5-card poker hands that can be made
	 * with the given starting hand.
	 */
	public static List<Hand> allHands(StartingHand startingHand) {
		Deck deck = new Deck().remove(startingHand.cards());
		List<List<Card>> combinations = new Combination(deck.cards(), 3).list();

		List<Hand> hands = new ArrayList<Hand>();
		for (List<Card> cards : combinations) {
			hands.add(Hand.newInstance(merge(cards, startingHand.cards())));
		}
		return hands;
	}

	/**
	 * Returns all possible 5-card poker hands that could be made with the given
	 * starting hand and board.
	 * 
	 * <p>
	 * The deck will contain 50 cards after one a hand has been dealt. The deck
	 * will contain 47 cards after the flop, 46 cards after the turn and 45
	 * cards after the river.
	 * </p>
	 * 
	 * <p>
	 * Once the community cards have been dealt, there are 21 (7c5) ways to
	 * choose a 5-card poker hand from the 5 community cards and 2 hole cards.
	 * The total number of hands that will be returned from this method are as
	 * follows:
	 * </p>
	 * 
	 * <p>
	 * If the board has 5 community cards:<br/>
	 * 21 hands -> 21 (7c5) * 1 (45c0)
	 * </p>
	 * 
	 * <p>
	 * If the board has 4 community cards:<br/>
	 * 966 hands -> 21 (7c5) * 46 (46c1)
	 * </p>
	 * 
	 * <p>
	 * If the board has 3 community cards:<br/>
	 * 22701 hands -> 21 (7c5) * 1081 (47c2)
	 * </p>
	 * 
	 * @return A List<Hand> of all possible 5-card poker hands that can be made
	 * with the given starting hand and board.
	 */
	public static List<Hand> allHands(StartingHand startingHand, Board board) {
		if (containsAny(startingHand.cards(), board.cards())) {
			throw new IllegalStateException("One or more cards belong to the starting hand and the board.");
		}

		// If we have a full board, simply enumerate through what's available.
		if (board.cards().size() == 5) {
			List<Card> spread = ListUtil.merge(startingHand.cards(), board.cards());
			return toHands(new Combination(spread, 5).list());
		}

		Deck deck = new Deck().remove(ListUtil.merge(startingHand.cards(), board.cards()));
		List<List<Card>> cards = new Combination(deck.cards(), 5 - board.cards().size()).list();

		List<Hand> hands = new ArrayList<Hand>();
		for (List<Card> top : cards) {
			List<Card> available = ListUtil.merge(startingHand.cards(), board.cards(), top);
			hands.addAll(toHands(new Combination(available, 5).list()));
		}
		return hands;
	}

	/**
	 * Returns all distinct 5-card poker hands.
	 * 
	 * @return A List<Hand> of distinct 5-card poker hands.
	 */
	public static List<Hand> distinctHands() {
		Map<Integer, Hand> map = new HashMap<Integer, Hand>();
		for (Hand hand : allHands()) {
			map.put(hand.key(), hand);
		}
		return new ArrayList<Hand>(map.values());
	}

	/**
	 * Returns all distinct 5-card poker hands that could be made with the given
	 * starting hand.
	 * 
	 * @return A List<Hand> of distinct 5-card poker hands that can be made with
	 * the given starting hand.
	 */
	public static List<Hand> distinctHands(StartingHand startingHand) {
		Map<Integer, Hand> uniqueHands = new HashMap<Integer, Hand>();
		for (Hand hand : allHands(startingHand)) {
			uniqueHands.put(hand.key(), hand);
		}
		return new ArrayList<Hand>(uniqueHands.values());
	}

	/**
	 * Returns all distinct 5-card poker hands that could be made with the given
	 * board and starting hand.
	 * 
	 * @return A List<Hand> of distinct 5-card poker hands that can be made with
	 * the given starting hand.
	 */
	public static List<Hand> distinctHands(StartingHand startingHand, Board board) {
		Map<Integer, Hand> map = new HashMap<Integer, Hand>();
		for (Hand hand : allHands(startingHand, board)) {
			map.put(hand.key(), hand);
		}
		return new ArrayList<Hand>(map.values());
	}

	private static List<Hand> toHands(List<List<Card>> cards) {
		List<Hand> hands = new ArrayList<Hand>();
		for (List<Card> instance : cards) {
			hands.add(Hand.newInstance(instance));
		}
		return hands;
	}

}
